// -----------------------------------------------------------------------------
// This source file is part of Matrix Platform
// 	(Universal .NET Software Development Platform)
// For the latest info, see http://www.matrixplatform.com
// 
// Copyright (c) 2009-2010, Ingenious Ltd
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

#if Matrix_Diagnostics
using Matrix.Common.Diagnostics;
#endif

namespace Matrix.Framework.DataStorage.DataSeries
{
    /// <summary>
    /// 
    /// </summary>
    public abstract class DataSeriesReaderRaw : DataSeriesManipulator
    {
        int _readPageSize = 32 * 1024;
        /// <summary>
        /// 32KB page size to read from the file while reading.
        /// Increase to ease manipulations with big objects.
        /// </summary>
        public int ReadPageSize
        {
            get { return _readPageSize; }
            set { _readPageSize = value; }
        }

        object _syncRoot = new object();

        public delegate bool RawPagedReadDelegate(DataSeriesReaderRaw reader, List<byte[]> data);

        /// <summary>
        /// 
        /// </summary>
        public DataSeriesReaderRaw()
        {
        }

        /// <summary>
        /// 
        /// </summary>
        public int RawReadPaged(ref long offset, int maxCountPerPage, int maxTotalCount, RawPagedReadDelegate delegateInstance)
        {
            int totalItemsRead = 0;

            List<byte[]> pageData;
            do
            {
                int readCount = maxCountPerPage;
                if (readCount + totalItemsRead > maxTotalCount)
                {// Last read.
                    readCount = maxTotalCount - totalItemsRead;
                }

                pageData = RawRead(ref offset, SeekOrigin.Begin, readCount, false);
                if (pageData != null && pageData.Count > 0)
                {
                    totalItemsRead += pageData.Count;

                    if (delegateInstance != null)
                    {
                        if (delegateInstance(this, pageData) == false)
                        {// Consumer requested to stop reading.
                            break;
                        }
                    }
                }
            }
            while (totalItemsRead < maxTotalCount
                   && (pageData != null && pageData.Count != 0));

            return totalItemsRead;
        }

        /// <summary>
        /// Since there may be a case where last item is not fully written to file, better to use last - 1, 
        /// to establish total items count or other.
        /// 
        /// TODO: automatically read the last item, even if it is bigger than page size.
        /// </summary>
        /// <param name="readBufferSize">Specify in case item buffer required to load items is bigger than 4 * ReadPageSize.</param>
        public byte[] RawReadLast(uint? stepBack)
        {
            DataSeriesSequenceHelper helper = SequenceHelper;
            if (helper == null)
            {
                return null;
            }

            //int pageSize = ReadPageSize * 4;
            long readBufferSize = ReadPageSize * 4;

            //if (readBufferSize.HasValue)
            //{
            //    readBufferSize = Math.Max(readBufferSize.Value, (int)Math.Max(0, pageSize));
            //}
            //else
            //{
            //    readBufferSize = (int)Math.Max(0, pageSize);
            //}

            if (_fileStream.Length < readBufferSize)
            {
                readBufferSize = Math.Min((int)_fileStream.Length, readBufferSize);
            }

            FileStream fileStream = _fileStream;
            if (fileStream == null)
            {
                return null;
            }

            long position;
            byte[] pageData;
            int bytesRead;

            lock (_syncRoot)
            {
                position = fileStream.Seek(fileStream.Length - readBufferSize, SeekOrigin.Begin);

                pageData = new byte[readBufferSize];
                bytesRead = fileStream.Read(pageData, 0, (int)readBufferSize);
            }

            List<long> escapes = helper.GetEscapeSequencePositions(pageData, 0, bytesRead);

            if (escapes == null || escapes.Count == 0)
            {
                return null;
            }

            long itemPosition = escapes[escapes.Count - 1] + (int)helper.EscapeSequence.Length;
            int itemSize = (int)bytesRead - (int)itemPosition;

            if (stepBack.HasValue && stepBack.Value > 0)
            {
                if (escapes.Count < stepBack + 1 || stepBack.Value < 0)
                {
                    return null;
                }

                itemPosition = escapes[escapes.Count - 1 - (int)stepBack.Value] + (int)helper.EscapeSequence.Length;
                itemSize = (int)escapes[escapes.Count - (int)stepBack.Value] - (int)itemPosition;
            }

            byte[] itemData = new byte[itemSize];

            MemoryStream stream = new MemoryStream(pageData);
            stream.Seek(itemPosition, SeekOrigin.Begin);
            int itemBytesRead = stream.Read(itemData, 0, itemData.Length);

            return itemData;
        }

        /// <summary>
        /// Read single item.
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="origin"></param>
        /// <returns></returns>
        public byte[] RawRead(ref long offset, SeekOrigin origin)
        {
            List<byte[]> result = RawRead(ref offset, origin, 1, false);

            if (result != null && result.Count > 0)
            {
                return result[0];
            }

            return null;
        }

        /// <summary>
        /// It will load all the data in memory while reading, so make sure to be below 
        /// OS limit for the application.
        /// 
        /// Extraction starts after the first escape, so if we seek to middle of an entry, we shall skip it.
        /// </summary>
        /// <param name="offset">Offset will be pointed to end of the last item we read from the file.</param>
        /// <param name="dummyRead">Only seek, do not actually read the data. Result will contain elements count, but each element is null.</param>
        /// <returns>Null if an error occurs while parsing (for ex. file is not properly formatted).</returns>
        public List<byte[]> RawRead(ref long offset, SeekOrigin origin, int maxCount, bool dummyRead)
        {
            FileStream fileStream = _fileStream;
            if (fileStream == null)
            {
                return null;
            }

            long startOffset;
            // Store actual separating escape positions here.
            List<long> separatorEscapes = new List<long>();
            MemoryStream combinedStream = new MemoryStream();
            int escapeSequenceLength;

            DataSeriesSequenceHelper sequenceHelper = SequenceHelper;

            lock (_syncRoot)
            {// Big lock, uses the sequenceHelper, but since it is lockless operation, it is OK.

                if (fileStream.Position != offset)
                {
                    long seekedTo = fileStream.Seek(offset, origin);
                    if (seekedTo != offset)
                    {// Failed to seek to desired position.
#if Matrix_Diagnostics
                        SystemMonitor.OperationError(string.Format("Reader failed to seek to position [{0}], managed only [{1}].", offset, seekedTo));
#endif
                        return null;
                    }
                }

                maxCount = Math.Max(0, maxCount);
                startOffset = fileStream.Position;

                escapeSequenceLength = sequenceHelper.EscapeSequence.Length;

                int bytesRead = 0;
                long startPosition = 0;

                do
                {// Load all the data into memory, enough to load all ements, or until max count.

                    byte[] pageData = new byte[ReadPageSize];
                    bytesRead = fileStream.Read(pageData, 0, ReadPageSize);

                    combinedStream.Write(pageData, 0, bytesRead);

                    List<long> escapesTemp = sequenceHelper.GetEscapeSequencePositions(combinedStream.GetBuffer(), startPosition, combinedStream.Length);

                    if (escapesTemp != null)
                    {
                        separatorEscapes.AddRange(escapesTemp);
                        startPosition = (int)separatorEscapes[separatorEscapes.Count - 1] + escapeSequenceLength;
                    }
                }
                while (bytesRead == ReadPageSize && separatorEscapes.Count - 1 < maxCount);

            } // Lock.

            if (separatorEscapes == null || separatorEscapes.Count == 0)
            {
                return null;
            }

            // TODO: optimize, only this line gives a delay of about 1 sec per million.
            // Renew all escape positions, since page reading may cause some of them to be skipped (if in the middle of a page).
            separatorEscapes = sequenceHelper.GetEscapeSequencePositions(combinedStream.GetBuffer(), 0, combinedStream.Length);

            // Reset the stream, since we shall read from it now.
            combinedStream.Seek(0, SeekOrigin.Begin);

            // Store the results here.
            List<byte[]> result = null;

            // Extract the results from the stream.
            for (int i = 0; i < separatorEscapes.Count && (result == null || result.Count < maxCount); i++)
            {
                long currenPosition = separatorEscapes[i];

                combinedStream.Seek(currenPosition + escapeSequenceLength, SeekOrigin.Begin);

                if (combinedStream.Position != currenPosition + escapeSequenceLength)
                {
#if Matrix_Diagnostics
                    SystemMonitor.Error("Position calculation failed.");
#endif
                    return null;
                }

                byte[] itemBuffer;
                int bytesReadCount;

                if (i < separatorEscapes.Count - 1)
                {// Any item.
                    itemBuffer = new byte[separatorEscapes[i + 1] - currenPosition - escapeSequenceLength];
                }
                else
                {// Last item.
                    itemBuffer = new byte[combinedStream.Length - currenPosition - escapeSequenceLength];
                }

                if (itemBuffer.Length == 0)
                {
#if Matrix_Diagnostics
                    SystemMonitor.Error("Invalid item buffer size.");
#endif
                    return null;
                }

                bytesReadCount = combinedStream.Read(itemBuffer, 0, (int)itemBuffer.Length);

                offset = startOffset + combinedStream.Position;

                if (itemBuffer.Length != bytesReadCount)
                {
#if Matrix_Diagnostics
                    SystemMonitor.Error("Invalid element positions calculation.");
#endif
                    return null;
                }
                else
                {
                    if (result == null)
                    {
                        result = new List<byte[]>();
                    }

                    if (dummyRead)
                    {// Just count them.
                        result.Add(null);
                    }
                    else
                    {
                        result.Add(itemBuffer);
                    }
                }
            }

            return result;
        }
    }
}
